Coverage Report

Created: 2026-04-21 11:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\scloud-dns\scloud-dns\src\config.rs
Line
Count
Source
1
//! Configuration types for scloud-dns
2
//!
3
//! This file contains Serde (Deserialize/Serialize) structs that map to the
4
//! JSON configuration you provided. It includes helpers to load the config
5
//! from a file and a light `validate()` method placeholder you can extend.
6
7
use crate::exceptions::SCloudException;
8
use anyhow::{Context, Result};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashSet;
11
use std::fs;
12
use std::path::Path;
13
14
/// Top-level configuration
15
#[derive(Debug, Clone, Serialize, Deserialize)]
16
pub struct Config {
17
    #[serde(default)]
18
    pub server: ServerConfig,
19
20
    #[serde(default)]
21
    pub workers: WorkersConfig,
22
23
    #[serde(default)]
24
    pub logging: LoggingConfig,
25
26
    #[serde(default)]
27
    pub metrics: MetricsConfig,
28
29
    #[serde(default)]
30
    pub admin: AdminConfig,
31
32
    #[serde(default)]
33
    pub acl: Vec<AclEntry>,
34
35
    #[serde(default)]
36
    pub listener: Vec<ListenerConfig>,
37
38
    #[serde(default)]
39
    pub doh: DohConfig,
40
41
    #[serde(default)]
42
    pub forwarder: Vec<ForwarderConfig>,
43
44
    #[serde(default)]
45
    pub root_hints: RootHintsConfig,
46
47
    #[serde(default)]
48
    pub cache: CacheConfig,
49
50
    #[serde(default)]
51
    pub recursion: RecursionConfig,
52
53
    #[serde(default)]
54
    pub ratelimit: RateLimitConfig,
55
56
    #[serde(default)]
57
    pub zone: Vec<ZoneConfig>,
58
59
    #[serde(default)]
60
    pub tsig_key: Vec<TsigKey>,
61
62
    #[serde(default)]
63
    pub axfr: AxfrConfig,
64
65
    #[serde(default)]
66
    pub dnssec: DnssecConfig,
67
68
    #[serde(default)]
69
    pub policy: PolicyConfig,
70
71
    #[serde(default)]
72
    pub amplification_mitigation: AmplificationMitigationConfig,
73
74
    #[serde(default)]
75
    pub tuning: TuningConfig,
76
77
    #[serde(default)]
78
    pub view: Vec<ViewConfig>,
79
80
    #[serde(default)]
81
    pub monitoring: MonitoringConfig,
82
83
    #[serde(default)]
84
    pub dynupdate: Vec<DynUpdateConfig>,
85
86
    #[serde(default)]
87
    pub limits: LimitsConfig,
88
}
89
90
impl Config {
91
    /// Load config from a JSON file path
92
9
    pub fn from_file(path: &Path) -> Result<Self, SCloudException> {
93
9
        let s = fs::read_to_string(path)
94
9
            .with_context(|| 
format!0
("reading config file {}",
path0
.
display0
()))
95
9
            .map_err(|_| SCloudException::SCLOUD_CONFIG_FILE_NOT_FOUND)
?0
;
96
9
        let cfg: Config = serde_json::from_str(&s)
97
9
            .context("parsing JSON config")
98
9
            .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_JSON)
?0
;
99
9
        cfg.validate()
?0
;
100
9
        Ok(cfg)
101
9
    }
102
103
    /// Validation hook
104
10
    pub fn validate(&self) -> Result<(), SCloudException> {
105
24
        let 
acl_names10
:
HashSet<&str>10
=
self.acl.iter()10
.
map10
(|a| a.name.as_str()).
collect10
();
106
16
        let 
tsig_names10
:
HashSet<&str>10
=
self.tsig_key.iter()10
.
map10
(|t| t.name.as_str()).
collect10
();
107
10
        let _forwarder_names: HashSet<&str> =
108
24
            
self.forwarder.iter()10
.
map10
(|f| f.name.as_str()).
collect10
();
109
110
72
        let 
is_acl_ref_valid10
= |s: &str| -> bool {
111
72
            if s.trim().is_empty() {
112
0
                return false;
113
72
            }
114
72
            acl_names.contains(s) || 
s16
.
contains16
('/')
115
72
        };
116
117
10
        if self.server.bind_port == 0 {
118
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_SERVER_PORT);
119
10
        }
120
10
        if self.server.max_udp_payload == 0 || self.server.max_udp_payload > 65535 {
121
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_MAX_UDP_PAYLOAD);
122
10
        }
123
10
        if self.tuning.max_label_length == 0 || self.tuning.max_label_length > 63 {
124
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
125
10
        }
126
10
        if self.tuning.max_domain_length == 0 || self.tuning.max_domain_length > 253 {
127
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
128
10
        }
129
10
        if self.limits.max_udp_packet_size == 0 || self.limits.max_udp_packet_size > 65535 {
130
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
131
10
        }
132
133
10
        let mut listener_names = HashSet::new();
134
24
        for l in 
&self.listener10
{
135
24
            if l.name.trim().is_empty() {
136
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER);
137
24
            }
138
24
            if !listener_names.insert(l.name.as_str()) {
139
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_LISTENER_NAME);
140
24
            }
141
24
            if l.port == 0 {
142
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PORT);
143
24
            }
144
24
            if l.protocols.is_empty() {
145
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PROTOCOLS);
146
24
            }
147
24
            if !l.acl.trim().is_empty() && !is_acl_ref_valid(&l.acl) {
148
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
149
24
            }
150
151
24
            if l.enable_tls.unwrap_or(false) {
152
8
                if l.tls_cert_path.as_deref().unwrap_or("").trim().is_empty() {
153
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT);
154
8
                }
155
8
                if l.tls_key_path.as_deref().unwrap_or("").trim().is_empty() {
156
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY);
157
8
                }
158
8
                if !l.protocols.iter().any(|p| matches!(p, Protocol::TCP)) {
159
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_REQUIRES_TCP);
160
8
                }
161
16
            }
162
        }
163
164
10
        if self.doh.enabled {
165
8
            if self.doh.paths.is_empty() {
166
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_DOH);
167
8
            }
168
8
            if self.doh.terminate_tls {
169
0
                if self
170
0
                    .doh
171
0
                    .tls_cert_path
172
0
                    .as_deref()
173
0
                    .unwrap_or("")
174
0
                    .trim()
175
0
                    .is_empty()
176
                {
177
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT);
178
0
                }
179
0
                if self
180
0
                    .doh
181
0
                    .tls_key_path
182
0
                    .as_deref()
183
0
                    .unwrap_or("")
184
0
                    .trim()
185
0
                    .is_empty()
186
                {
187
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY);
188
0
                }
189
8
            }
190
2
        }
191
192
10
        if self.recursion.enabled {
193
8
            if self.recursion.allowed_acl.trim().is_empty() {
194
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
195
8
            }
196
8
            if !is_acl_ref_valid(&self.recursion.allowed_acl) {
197
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
198
8
            }
199
2
        }
200
201
10
        let mut fwd_names = HashSet::new();
202
24
        for f in 
&self.forwarder10
{
203
24
            if f.name.trim().is_empty() {
204
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER);
205
24
            }
206
24
            if !fwd_names.insert(f.name.as_str()) {
207
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_FORWARDER_NAME);
208
24
            }
209
24
            if f.addresses.is_empty() {
210
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER);
211
24
            }
212
40
            for a in 
&f.addresses24
{
213
40
                if a.parse::<std::net::SocketAddr>().is_err() {
214
0
                    return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
215
40
                }
216
            }
217
        }
218
219
10
        let mut zone_names = HashSet::new();
220
32
        for z in 
&self.zone10
{
221
32
            if z.name.trim().is_empty() {
222
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_ZONE);
223
32
            }
224
32
            if !zone_names.insert(z.name.as_str()) {
225
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_ZONE_NAME);
226
32
            }
227
228
32
            match z.kind {
229
                ZoneType::Master => {
230
16
                    let inline = z.inline.unwrap_or(false);
231
16
                    if inline {
232
8
                        if z.records.is_empty() {
233
0
                            return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE);
234
8
                        }
235
8
                        let has_soa = z
236
8
                            .records
237
8
                            .iter()
238
8
                            .any(|r| r.r#type.eq_ignore_ascii_case("SOA"));
239
8
                        if !has_soa {
240
0
                            return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE);
241
8
                        }
242
                    } else {
243
8
                        if z.file.as_deref().unwrap_or("").trim().is_empty() {
244
0
                            return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE);
245
8
                        }
246
                    }
247
248
16
                    if let Some(
acl8
) = z.notify_acl.as_deref() {
249
8
                        if !acl.trim().is_empty() && !is_acl_ref_valid(acl) {
250
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
251
8
                        }
252
8
                    }
253
16
                    if let Some(
acl8
) = z.allow_transfer_acl.as_deref() {
254
8
                        if !acl.trim().is_empty() && !is_acl_ref_valid(acl) {
255
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
256
8
                        }
257
8
                    }
258
259
16
                    if let Some(
k8
) = z.axfr_tsig_key.as_deref() {
260
8
                        if !k.trim().is_empty() && !tsig_names.contains(k) {
261
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY);
262
8
                        }
263
8
                    }
264
                }
265
                ZoneType::Slave => {
266
8
                    if z.masters.is_empty() {
267
0
                        return Err(SCloudException::SCLOUD_CONFIG_SLAVE_MISSING_MASTERS);
268
8
                    }
269
8
                    for m in &z.masters {
270
8
                        if m.parse::<std::net::SocketAddr>().is_err() {
271
0
                            return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
272
8
                        }
273
                    }
274
8
                    if z.file.as_deref().unwrap_or("").trim().is_empty() {
275
0
                        return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE);
276
8
                    }
277
                }
278
                ZoneType::Forward => {
279
8
                    if z.forwarders.is_empty() {
280
0
                        return Err(SCloudException::SCLOUD_CONFIG_FORWARD_ZONE_MISSING_FORWARDERS);
281
8
                    }
282
8
                    for f in &z.forwarders {
283
8
                        if f.parse::<std::net::SocketAddr>().is_err() {
284
0
                            return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
285
8
                        }
286
                    }
287
                }
288
0
                ZoneType::Stub => {
289
0
                    // TODO: not defined JSON yet, strict checks later when I will implement it.
290
0
                }
291
            }
292
293
48
            for r in 
&z.records32
{
294
48
                if r.r#type.eq_ignore_ascii_case("MX") {
295
8
                    if r.priority.is_none() {
296
0
                        return Err(SCloudException::SCLOUD_CONFIG_MX_MISSING_PRIORITY);
297
8
                    }
298
40
                } else if r.priority.is_some() {
299
0
                    return Err(SCloudException::SCLOUD_CONFIG_PRIORITY_ON_NON_MX);
300
40
                }
301
            }
302
        }
303
304
10
        let mut view_names = HashSet::new();
305
16
        for v in 
&self.view10
{
306
16
            if v.name.trim().is_empty() {
307
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW);
308
16
            }
309
16
            if !view_names.insert(v.name.as_str()) {
310
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_VIEW_NAME);
311
16
            }
312
16
            if v.acl.trim().is_empty() || !is_acl_ref_valid(&v.acl) {
313
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
314
16
            }
315
16
            for vz in &v.zones {
316
16
                if vz.name.trim().is_empty() || vz.file.trim().is_empty() {
317
0
                    return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW);
318
16
                }
319
            }
320
        }
321
322
10
        for 
d8
in &self.dynupdate {
323
8
            if d.zone.trim().is_empty() {
324
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_DYNUPDATE);
325
8
            }
326
8
            if d.acl.trim().is_empty() || !is_acl_ref_valid(&d.acl) {
327
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
328
8
            }
329
8
            if let Some(k) = d.tsig_key.as_deref() {
330
8
                if !k.trim().is_empty() && !tsig_names.contains(k) {
331
0
                    return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY);
332
8
                }
333
0
            }
334
335
8
            if !zone_names.contains(d.zone.as_str()) {
336
0
                return Err(SCloudException::SCLOUD_CONFIG_DYNUPDATE_UNKNOWN_ZONE);
337
8
            }
338
        }
339
340
10
        Ok(())
341
10
    }
342
343
    /// Get the address of a specific forwarder by index value
344
    #[allow(unused)]
345
5
    pub(crate) fn try_get_forwarder_addr_by_index(
346
5
        &self,
347
5
        forwarder_index: usize,
348
5
        address_index: usize,
349
5
    ) -> Result<std::net::SocketAddr, SCloudException> {
350
5
        let addr = self
351
5
            .forwarder
352
5
            .get(forwarder_index)
353
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)
?0
354
            .addresses
355
5
            .get(address_index)
356
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_ADDRESS)
?0
357
5
            .parse()
358
5
            .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)
?0
;
359
360
5
        Ok(addr)
361
5
    }
362
363
    // TODO: add a loop to test the next address for each retry
364
5
    pub(crate) fn try_get_forwarder_addr_by_name(
365
5
        &self,
366
5
        forwarder_name: &str,
367
5
    ) -> Result<std::net::SocketAddr, SCloudException> {
368
5
        let forwarder = self
369
5
            .forwarder
370
5
            .iter()
371
12
            .
find5
(|f| f.name == forwarder_name)
372
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)
?0
;
373
374
5
        for addr_str in &forwarder.addresses {
375
5
            if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() {
376
5
                return Ok(addr);
377
0
            }
378
        }
379
380
0
        Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)
381
5
    }
382
}
383
384
impl Default for Config {
385
5
    fn default() -> Self {
386
5
        Self {
387
5
            server: ServerConfig::default(),
388
5
            workers: WorkersConfig::default(),
389
5
            logging: LoggingConfig::default(),
390
5
            metrics: MetricsConfig::default(),
391
5
            admin: AdminConfig::default(),
392
5
            acl: Vec::new(),
393
5
            listener: Vec::new(),
394
5
            doh: DohConfig::default(),
395
5
            forwarder: Vec::new(),
396
5
            root_hints: RootHintsConfig::default(),
397
5
            cache: CacheConfig::default(),
398
5
            recursion: RecursionConfig::default(),
399
5
            ratelimit: RateLimitConfig::default(),
400
5
            zone: Vec::new(),
401
5
            tsig_key: Vec::new(),
402
5
            axfr: AxfrConfig::default(),
403
5
            dnssec: DnssecConfig::default(),
404
5
            policy: PolicyConfig::default(),
405
5
            amplification_mitigation: AmplificationMitigationConfig::default(),
406
5
            tuning: TuningConfig::default(),
407
5
            view: Vec::new(),
408
5
            monitoring: MonitoringConfig::default(),
409
5
            dynupdate: Vec::new(),
410
5
            limits: LimitsConfig::default(),
411
5
        }
412
5
    }
413
}
414
415
#[derive(Debug, Clone, Serialize, Deserialize)]
416
pub struct ServerConfig {
417
    pub name: String,
418
    pub version: String,
419
    pub environment: String,
420
    pub max_concurrent_requests: usize,
421
    pub graceful_shutdown_timeout_secs: u64,
422
423
    pub default_ttl: u32,
424
    pub max_udp_payload: usize,
425
    pub enable_edns: bool,
426
    pub enable_tcp: bool,
427
    pub enable_dnssec: bool,
428
429
    pub bind_port: u16,
430
}
431
432
impl Default for ServerConfig {
433
6
    fn default() -> Self {
434
6
        ServerConfig {
435
6
            name: "scloud-dns".to_string(),
436
6
            version: "none".to_string(),
437
6
            environment: "production".to_string(),
438
6
            max_concurrent_requests: 5000,
439
6
            graceful_shutdown_timeout_secs: 15,
440
6
            default_ttl: 3600,
441
6
            max_udp_payload: 4096,
442
6
            enable_edns: true,
443
6
            enable_tcp: true,
444
6
            enable_dnssec: false,
445
6
            bind_port: 53,
446
6
        }
447
6
    }
448
}
449
450
#[derive(Debug, Clone, Serialize, Deserialize)]
451
pub struct WorkersConfig {
452
    pub tcp_acceptor: u16,
453
    #[serde(default)]
454
    pub doh_acceptor: u16,
455
    pub decoder: u16,
456
    pub query_dispatcher: u16,
457
    pub cache_lookup: u16,
458
    pub zone_manager: u16,
459
    pub resolver: u16,
460
    pub cache_writer: u16,
461
    pub encoder: u16,
462
    pub sender: u16,
463
    pub cache_janitor: u16,
464
    pub metrics: u16,
465
}
466
467
impl Default for WorkersConfig {
468
5
    fn default() -> Self {
469
5
        WorkersConfig {
470
5
            tcp_acceptor: 1,
471
5
            doh_acceptor: 1,
472
5
            decoder: 5,
473
5
            query_dispatcher: 3,
474
5
            cache_lookup: 3,
475
5
            zone_manager: 1,
476
5
            resolver: 5,
477
5
            cache_writer: 1,
478
5
            encoder: 5,
479
5
            sender: 5,
480
5
            cache_janitor: 1,
481
5
            metrics: 2,
482
5
        }
483
5
    }
484
}
485
486
#[derive(Debug, Clone, Serialize, Deserialize)]
487
pub struct LoggingConfig {
488
    pub level: LogLevel,
489
    pub format: LogFormat,
490
    pub file: String,
491
    pub dyn_ui: bool,
492
    pub rotate: bool,
493
    pub live_print: bool,
494
    pub max_size_mb: u64,
495
}
496
497
impl Default for LoggingConfig {
498
5
    fn default() -> Self {
499
5
        LoggingConfig {
500
5
            level: LogLevel::INFO,
501
5
            format: LogFormat::TEXT,
502
5
            file: "/var/log/scloud-dns/scloud-dns.log".to_string(),
503
5
            dyn_ui: false,
504
5
            rotate: true,
505
5
            live_print: false,
506
5
            max_size_mb: 200,
507
5
        }
508
5
    }
509
}
510
511
#[allow(non_camel_case_types)]
512
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
513
#[serde(rename_all = "lowercase")]
514
pub enum LogLevel {
515
    TRACE = 0,
516
    DEBUG = 1,
517
    INFO = 2,
518
    WARN = 3,
519
    ERROR = 4,
520
    FATAL = 5,
521
}
522
523
impl LogLevel {
524
0
    pub fn parse(s: &str) -> Self {
525
0
        match s.to_ascii_lowercase().as_str() {
526
0
            "trace" => Self::TRACE,
527
0
            "debug" => Self::DEBUG,
528
0
            "info" => Self::INFO,
529
0
            "warn" | "warning" => Self::WARN,
530
0
            "error" => Self::ERROR,
531
0
            "fatal" => Self::FATAL,
532
0
            _ => Self::WARN,
533
        }
534
0
    }
535
536
25
    pub(crate) fn as_str(self) -> &'static str {
537
25
        match self {
538
1
            Self::TRACE => "trace",
539
19
            Self::DEBUG => "debug",
540
4
            Self::INFO => "info",
541
0
            Self::WARN => "warn",
542
1
            Self::ERROR => "error",
543
0
            Self::FATAL => "fatal",
544
        }
545
25
    }
546
}
547
548
#[allow(non_camel_case_types)]
549
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
550
#[serde(rename_all = "lowercase")]
551
pub enum LogFormat {
552
    JSON,
553
    TEXT,
554
}
555
556
impl LogFormat {
557
0
    pub fn parse(s: &str) -> Self {
558
0
        match s.to_ascii_lowercase().as_str() {
559
0
            "json" => Self::JSON,
560
0
            _ => Self::TEXT,
561
        }
562
0
    }
563
}
564
565
#[derive(Debug, Clone, Serialize, Deserialize)]
566
pub struct MetricsConfig {
567
    pub enabled: bool,
568
    pub prometheus_bind: String,
569
    pub enable_health_endpoint: bool,
570
    pub health_bind: String,
571
}
572
573
impl Default for MetricsConfig {
574
5
    fn default() -> Self {
575
5
        MetricsConfig {
576
5
            enabled: true,
577
5
            prometheus_bind: "0.0.0.0:9153".to_string(),
578
5
            enable_health_endpoint: true,
579
5
            health_bind: "127.0.0.1:8081".to_string(),
580
5
        }
581
5
    }
582
}
583
584
#[derive(Debug, Clone, Serialize, Deserialize)]
585
pub struct AdminConfig {
586
    pub enabled: bool,
587
    pub bind: String,
588
    pub auth_token: String,
589
    pub enable_tls: bool,
590
}
591
592
impl Default for AdminConfig {
593
5
    fn default() -> Self {
594
5
        AdminConfig {
595
5
            enabled: true,
596
5
            bind: "127.0.0.1:8053".to_string(),
597
5
            auth_token: "replace-with-secure-token".to_string(),
598
5
            enable_tls: false,
599
5
        }
600
5
    }
601
}
602
603
#[derive(Debug, Clone, Serialize, Deserialize)]
604
pub struct AclEntry {
605
    pub name: String,
606
    pub networks: Vec<String>, // CIDRs or single IPs; parse later with ipnet or similar
607
}
608
609
#[derive(Debug, Clone, Serialize, Deserialize)]
610
pub struct ListenerConfig {
611
    pub name: String,
612
    pub address: String,
613
    pub port: u16,
614
    #[serde(default)]
615
    pub protocols: Vec<Protocol>,
616
    #[serde(default)]
617
    pub recursion_allowed: bool,
618
    /// ACL name or a raw CIDR/list string
619
    #[serde(default)]
620
    pub acl: String,
621
    #[serde(default)]
622
    pub workers: Option<usize>,
623
    #[serde(default)]
624
    pub enable_tls: Option<bool>,
625
    #[serde(default)]
626
    pub tls_cert_path: Option<String>,
627
    #[serde(default)]
628
    pub tls_key_path: Option<String>,
629
}
630
631
impl Default for ListenerConfig {
632
1
    fn default() -> Self {
633
1
        ListenerConfig {
634
1
            name: String::new(),
635
1
            address: "0.0.0.0".to_string(),
636
1
            port: 53,
637
1
            protocols: vec![Protocol::UDP],
638
1
            recursion_allowed: false,
639
1
            acl: "0.0.0.0/0".to_string(),
640
1
            workers: None,
641
1
            enable_tls: None,
642
1
            tls_cert_path: None,
643
1
            tls_key_path: None,
644
1
        }
645
1
    }
646
}
647
648
#[derive(Debug, Clone, Serialize, Deserialize)]
649
#[serde(rename_all = "lowercase")]
650
pub enum Protocol {
651
    UDP,
652
    TCP,
653
}
654
655
#[derive(Debug, Clone, Serialize, Deserialize)]
656
pub struct DohConfig {
657
    pub enabled: bool,
658
    pub bind: String,
659
    #[serde(default)]
660
    pub terminate_tls: bool,
661
    #[serde(default)]
662
    pub tls_cert_path: Option<String>,
663
    #[serde(default)]
664
    pub tls_key_path: Option<String>,
665
    #[serde(default)]
666
    pub paths: Vec<String>,
667
    #[serde(default)]
668
    pub allowed_origins: Vec<String>,
669
}
670
671
impl Default for DohConfig {
672
6
    fn default() -> Self {
673
6
        DohConfig {
674
6
            enabled: false,
675
6
            bind: "0.0.0.0:8053".to_string(),
676
6
            terminate_tls: false,
677
6
            tls_cert_path: None,
678
6
            tls_key_path: None,
679
6
            paths: vec!["/dns-query".to_string()],
680
6
            allowed_origins: Vec::new(),
681
6
        }
682
6
    }
683
}
684
685
#[derive(Debug, Clone, Serialize, Deserialize)]
686
pub struct ForwarderConfig {
687
    pub name: String,
688
    pub addresses: Vec<String>,
689
    pub policy: ForwardPolicy,
690
    pub timeout_ms: u64,
691
    pub edns: bool,
692
    pub use_tcp_on_retry: Option<bool>,
693
}
694
695
impl Default for ForwarderConfig {
696
1
    fn default() -> Self {
697
1
        ForwarderConfig {
698
1
            name: String::new(),
699
1
            addresses: Vec::new(),
700
1
            policy: ForwardPolicy::First,
701
1
            timeout_ms: 1500,
702
1
            edns: true,
703
1
            use_tcp_on_retry: Some(true),
704
1
        }
705
1
    }
706
}
707
708
#[derive(Debug, Clone, Serialize, Deserialize)]
709
#[serde(rename_all = "snake_case")]
710
#[derive(PartialEq)]
711
pub enum ForwardPolicy {
712
    RoundRobin,
713
    First,
714
    Random,
715
}
716
717
#[derive(Debug, Clone, Serialize, Deserialize)]
718
pub struct RootHintsConfig {
719
    pub file: String,
720
}
721
722
impl Default for RootHintsConfig {
723
5
    fn default() -> Self {
724
5
        RootHintsConfig {
725
5
            file: "/etc/scloud/root.hints".to_string(),
726
5
        }
727
5
    }
728
}
729
730
#[derive(Debug, Clone, Serialize, Deserialize)]
731
pub struct CacheConfig {
732
    pub enabled: bool,
733
    pub max_entries: usize,
734
    pub max_ttl_seconds: u64,
735
    pub negative_ttl_seconds: u64,
736
    pub eviction_policy: String,
737
}
738
739
impl Default for CacheConfig {
740
6
    fn default() -> Self {
741
6
        CacheConfig {
742
6
            enabled: true,
743
6
            max_entries: 200_000,
744
6
            max_ttl_seconds: 86_400,
745
6
            negative_ttl_seconds: 300,
746
6
            eviction_policy: "lru".to_string(),
747
6
        }
748
6
    }
749
}
750
751
#[derive(Debug, Clone, Serialize, Deserialize)]
752
pub struct RecursionConfig {
753
    pub enabled: bool,
754
    pub allowed_acl: String,
755
    pub max_recursive_queries: usize,
756
    pub recursion_timeout_ms: u64,
757
    pub retry_interval_ms: u64,
758
}
759
760
impl Default for RecursionConfig {
761
6
    fn default() -> Self {
762
6
        RecursionConfig {
763
6
            enabled: false,
764
6
            allowed_acl: "internal".to_string(),
765
6
            max_recursive_queries: 50,
766
6
            recursion_timeout_ms: 5000,
767
6
            retry_interval_ms: 200,
768
6
        }
769
6
    }
770
}
771
772
#[derive(Debug, Clone, Serialize, Deserialize)]
773
pub struct RateLimitConfig {
774
    pub enabled: bool,
775
    pub global_qps: u64,
776
    pub per_ip_qps: u64,
777
    pub per_subnet_qps: u64,
778
    pub rrl: RrlConfig,
779
}
780
781
impl Default for RateLimitConfig {
782
6
    fn default() -> Self {
783
6
        RateLimitConfig {
784
6
            enabled: true,
785
6
            global_qps: 3000,
786
6
            per_ip_qps: 100,
787
6
            per_subnet_qps: 1000,
788
6
            rrl: RrlConfig::default(),
789
6
        }
790
6
    }
791
}
792
793
#[derive(Debug, Clone, Serialize, Deserialize)]
794
pub struct RrlConfig {
795
    pub enabled: bool,
796
    pub window_seconds: u64,
797
    pub slip: u32,
798
    pub qps_threshold: u64,
799
}
800
801
impl Default for RrlConfig {
802
6
    fn default() -> Self {
803
6
        RrlConfig {
804
6
            enabled: true,
805
6
            window_seconds: 5,
806
6
            slip: 2,
807
6
            qps_threshold: 50,
808
6
        }
809
6
    }
810
}
811
812
#[derive(Debug, Clone, Serialize, Deserialize)]
813
pub struct ZoneConfig {
814
    pub name: String,
815
    #[serde(rename = "type")]
816
    pub kind: ZoneType,
817
    #[serde(default)]
818
    pub file: Option<String>,
819
    #[serde(default)]
820
    pub notify: Option<bool>,
821
    #[serde(default)]
822
    pub notify_acl: Option<String>,
823
    #[serde(default)]
824
    pub allow_transfer_acl: Option<String>,
825
    #[serde(default)]
826
    pub allow_update_acl: Option<String>,
827
    #[serde(default)]
828
    pub axfr_tsig_key: Option<String>,
829
830
    // Slave-specific
831
    #[serde(default)]
832
    pub masters: Vec<String>,
833
834
    // Inline zone
835
    #[serde(default)]
836
    pub inline: Option<bool>,
837
    #[serde(default)]
838
    pub records: Vec<ZoneRecord>,
839
840
    // Forward-specific
841
    #[serde(default)]
842
    pub forwarders: Vec<String>,
843
    #[serde(default)]
844
    pub forward_policy: Option<String>,
845
}
846
847
impl Default for ZoneConfig {
848
1
    fn default() -> Self {
849
1
        ZoneConfig {
850
1
            name: String::new(),
851
1
            kind: ZoneType::Master,
852
1
            file: None,
853
1
            notify: Some(false),
854
1
            notify_acl: None,
855
1
            allow_transfer_acl: None,
856
1
            allow_update_acl: None,
857
1
            axfr_tsig_key: None,
858
1
            masters: Vec::new(),
859
1
            inline: Some(false),
860
1
            records: Vec::new(),
861
1
            forwarders: Vec::new(),
862
1
            forward_policy: None,
863
1
        }
864
1
    }
865
}
866
867
#[derive(Debug, Clone, Serialize, Deserialize)]
868
#[serde(rename_all = "lowercase")]
869
#[derive(PartialEq)]
870
pub enum ZoneType {
871
    Master,
872
    Slave,
873
    Forward,
874
    Stub,
875
}
876
877
#[derive(Debug, Clone, Serialize, Deserialize)]
878
pub struct ZoneRecord {
879
    pub name: String,
880
    pub ttl: Option<u32>,
881
    pub class: Option<String>,
882
    #[serde(rename = "type")]
883
    pub r#type: String,
884
    pub rdata: String,
885
    #[serde(default)]
886
    pub priority: Option<u16>,
887
}
888
889
#[derive(Debug, Clone, Serialize, Deserialize)]
890
pub struct TsigKey {
891
    pub name: String,
892
    pub algorithm: String,
893
    pub secret: String, // TODO: base64 encoded - do not keep in plaintext in production
894
}
895
896
#[derive(Debug, Clone, Serialize, Deserialize)]
897
pub struct AxfrConfig {
898
    pub enabled: bool,
899
    pub max_concurrent_transfers: usize,
900
    pub transfer_timeout_secs: u64,
901
}
902
903
impl Default for AxfrConfig {
904
6
    fn default() -> Self {
905
6
        AxfrConfig {
906
6
            enabled: true,
907
6
            max_concurrent_transfers: 4,
908
6
            transfer_timeout_secs: 120,
909
6
        }
910
6
    }
911
}
912
913
#[derive(Debug, Clone, Serialize, Deserialize)]
914
pub struct DnssecConfig {
915
    pub enabled: bool,
916
    pub auto_sign: bool,
917
    pub default_algo: String,
918
    pub kasp_file: Option<String>,
919
}
920
921
impl Default for DnssecConfig {
922
6
    fn default() -> Self {
923
6
        DnssecConfig {
924
6
            enabled: false,
925
6
            auto_sign: false,
926
6
            default_algo: "RSASHA256".to_string(),
927
6
            kasp_file: None,
928
6
        }
929
6
    }
930
}
931
932
#[derive(Debug, Clone, Serialize, Deserialize)]
933
pub struct PolicyConfig {
934
    #[serde(default)]
935
    pub deny_domains: Vec<String>,
936
}
937
938
impl Default for PolicyConfig {
939
5
    fn default() -> Self {
940
5
        PolicyConfig {
941
5
            deny_domains: Vec::new(),
942
5
        }
943
5
    }
944
}
945
946
#[derive(Debug, Clone, Serialize, Deserialize)]
947
pub struct AmplificationMitigationConfig {
948
    pub drop_fragments: bool,
949
    pub max_response_size_udp: usize,
950
}
951
952
impl Default for AmplificationMitigationConfig {
953
5
    fn default() -> Self {
954
5
        AmplificationMitigationConfig {
955
5
            drop_fragments: true,
956
5
            max_response_size_udp: 4096,
957
5
        }
958
5
    }
959
}
960
961
#[derive(Debug, Clone, Serialize, Deserialize)]
962
pub struct TuningConfig {
963
    pub socket_recv_buffer_bytes: usize,
964
    pub socket_send_buffer_bytes: usize,
965
    pub max_label_length: usize,
966
    pub max_domain_length: usize,
967
}
968
969
impl Default for TuningConfig {
970
5
    fn default() -> Self {
971
5
        TuningConfig {
972
5
            socket_recv_buffer_bytes: 262_144,
973
5
            socket_send_buffer_bytes: 262_144,
974
5
            max_label_length: 63,
975
5
            max_domain_length: 253,
976
5
        }
977
5
    }
978
}
979
980
#[derive(Debug, Clone, Serialize, Deserialize)]
981
pub struct ViewConfig {
982
    pub name: String,
983
    pub acl: String,
984
    #[serde(default)]
985
    pub zones: Vec<ViewZone>,
986
}
987
988
#[derive(Debug, Clone, Serialize, Deserialize)]
989
pub struct ViewZone {
990
    pub name: String,
991
    pub file: String,
992
}
993
994
#[derive(Debug, Clone, Serialize, Deserialize)]
995
pub struct MonitoringConfig {
996
    pub enable_query_logging: bool,
997
    pub query_log_path: String,
998
    pub log_query_qps: u64,
999
}
1000
1001
impl Default for MonitoringConfig {
1002
5
    fn default() -> Self {
1003
5
        MonitoringConfig {
1004
5
            enable_query_logging: false,
1005
5
            query_log_path: "/var/log/scloud-dns/queries.log".to_string(),
1006
5
            log_query_qps: 1000,
1007
5
        }
1008
5
    }
1009
}
1010
1011
#[derive(Debug, Clone, Serialize, Deserialize)]
1012
pub struct DynUpdateConfig {
1013
    pub zone: String,
1014
    pub acl: String,
1015
    pub tsig_key: Option<String>,
1016
    pub allow: bool,
1017
}
1018
1019
#[derive(Debug, Clone, Serialize, Deserialize)]
1020
pub struct LimitsConfig {
1021
    pub max_udp_packet_size: usize,
1022
    pub max_queries_per_minute_per_ip: u64,
1023
    pub max_tcp_sessions_per_ip: usize,
1024
}
1025
1026
impl Default for LimitsConfig {
1027
6
    fn default() -> Self {
1028
6
        LimitsConfig {
1029
6
            max_udp_packet_size: 4096,
1030
6
            max_queries_per_minute_per_ip: 1000,
1031
6
            max_tcp_sessions_per_ip: 8,
1032
6
        }
1033
6
    }
1034
}